/****************************************************************************
 * Copyright (c) 2010 Torkild U. Resheim
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Torkild U. Resheim  - initial API and implementation
 *****************************************************************************/
package no.resheim.buildmonitor.hudson;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ChoiceFormat;
import java.text.MessageFormat;
import java.util.HashMap;

import no.resheim.buildmonitor.core.data.Build;
import no.resheim.buildmonitor.core.data.Job;

import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.ecf.core.IContainer;
import org.eclipse.ecf.core.util.ECFException;
import org.eclipse.ecf.remoteservice.IRemoteService;
import org.eclipse.ecf.remoteservice.IRemoteServiceRegistration;
import org.eclipse.ecf.remoteservice.client.IRemoteCallParameter;
import org.eclipse.ecf.remoteservice.client.IRemoteCallable;
import org.eclipse.ecf.remoteservice.client.IRemoteServiceClientContainerAdapter;
import org.eclipse.ecf.remoteservice.client.RemoteCallParameterFactory;
import org.eclipse.ecf.remoteservice.rest.RestCallFactory;
import org.eclipse.ecf.remoteservice.rest.RestCallableFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.api.client.escape.CharEscapers;

/**
 * Variant of {@link Job} that describes a Hudson build job
 * 
 * @author Torkild Ulvøy Resheim
 * @since 1.0
 */
public class HudsonJob extends Job {

	private final IContainer container;

	/** A list of all builds we have knowledge of. This is a local cache. */
	protected final HashMap<Integer, Build> builds;

	public HudsonJob(IContainer container, JSONObject source)
			throws URISyntaxException, JSONException {
		super(URI.create(source.getString("url")));
		builds = new HashMap<Integer, Build>();
		this.container = container;
		decode(source);
	}

	@Override
	public void build() {
		execute("build?delay=0sec", "tempAction");
	}

	void decode(JSONObject source) throws JSONException, URISyntaxException {
		setDescription(source.getString("description"));
		setDisplayName(source.getString("displayName"));
		setName(source.getString("name"));

		if (!source.isNull("firstBuild")) {
			setFirstBuildNumber(source.getJSONObject("firstBuild").getInt(
					"number"));
		}
		if (!source.isNull("lastBuild")) {
			setLastStartedBuildNumber(source.getJSONObject("lastBuild").getInt(
					"number"));
		}
		if (!source.isNull("lastCompletedBuild")) {
			setLastCompletedBuildNumber(source.getJSONObject(
					"lastCompletedBuild").getInt("number"));
			// Obtain the full build description
		}
		if (!source.isNull("lastFailedBuild")) {
			setLastFailedBuildNumber(source.getJSONObject("lastFailedBuild")
					.getInt("number"));
		}
		if (!source.isNull("lastStableBuild")) {
			setLastStableBuildNumber(source.getJSONObject("lastStableBuild")
					.getInt("number"));
			// Should be in list already
		}
		if (!source.isNull("lastSuccessfullBuild")) {
			setLastSuccessfulBuildNumber(source.getJSONObject(
					"lastSuccessfullBuild").getInt("number"));
			// Should be in list already
		}
		if (!source.isNull("healthReport")) {
			JSONArray list = source.getJSONArray("healthReport");
			if (list.length() > 0) {
				setHealthReport(list.getJSONObject(0).getString("description"));
			}
		}
		decodeState(source.getString("color"), source.getBoolean("inQueue"));
	}

	/**
	 * Hudson uses colours to indicate the state of a build job. This method
	 * will use the "colour" to set the build status. It will also determine the
	 * <i>changeDescription</i> of the job. This can be used in the UI do
	 * describe the latest change to the job state.
	 * 
	 * @param color
	 *            the colour name
	 */
	private void decodeState(String color, boolean newQueued) {
		// Clear the description first
		setChangeDescription(null);
		ChoiceFormat stoppedStates = new ChoiceFormat(
				"0#stopped with an error|1#stopped with test failures|2#successfully finished|3#been aborted|4#been disabled|5#an unknown state");
		ChoiceFormat startedStates = new ChoiceFormat(
				"0#started|1#started|2#started|3#restarted|4#started|5#started");
		Health newHealth = null;
		if (color.startsWith("red"))
			newHealth = Health.ERROR;
		if (color.startsWith("yellow"))
			newHealth = Health.FAILED;
		if (color.startsWith("blue"))
			newHealth = Health.GOOD;
		if (color.startsWith("aborted"))
			newHealth = Health.ABORTED;
		if (color.startsWith("disabled"))
			newHealth = Health.DISABLED;
		if (color.startsWith("grey"))
			newHealth = Health.UNKNOWN;
		if (!newHealth.equals(getHealth())) {
			setChanged();
			setHealth(newHealth);
		}
		boolean newActive = color.endsWith("_anime");
		if (newActive != isActive()) {
			setChanged();
			setActive(newActive);
			if (newActive) {
				setChangeDescription(MessageFormat.format(
						"{0} build #{1} has {2}.",
						new Object[] { getName(), getLastStartedBuildNumber(),
								startedStates.format(newHealth.ordinal()) }));
			} else {
				setChangeDescription(MessageFormat.format(
						"{0} build #{1} has {2}.",
						new Object[] { getName(), getLastStartedBuildNumber(),
								stoppedStates.format(newHealth.ordinal()) }));
			}
		}
		if (newQueued != isInQueue()) {
			setChanged();
			setInQueue(newQueued);
			if (newQueued) {
				setChangeDescription(MessageFormat.format(
						"{0} build #{1} has been queued.", new Object[] {
								getName(), getLastStartedBuildNumber() }));
			}
		}
	}

	/**
	 * This method is used to execute actions on the job. The ECF REST API is
	 * used in a way that I'm not comfortable with. Maybe some things can be
	 * done better?
	 * 
	 * @param action
	 */
	private Object execute(String action, String id) {
		try {
			// We won't get anything back from the server when doing this call.
			// Is there any way?
			IRemoteCallable query = RestCallableFactory
					.createCallable(id, CharEscapers
							.escapeUriQuery(MessageFormat.format(
									"/job/{0}/{1}", new Object[] { getName(),
											action })));
			IRemoteServiceClientContainerAdapter adapter = (IRemoteServiceClientContainerAdapter) container
					.getAdapter(IRemoteServiceClientContainerAdapter.class);
			IRemoteServiceRegistration registration = adapter
					.registerCallables(
							new String[] { IHudsonQuery.class.getName() },
							new IRemoteCallable[][] { { query } }, null);
			IRemoteService restClientService = adapter
					.getRemoteService(registration.getReference());
			Object object = restClientService.callSync(RestCallFactory
					.createRestCall(IHudsonQuery.class.getName() + "." + id));
			// registration.unregister();
			return object;
		} catch (ECFException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * Connects to the Hudson server and obtains build details in the form of a
	 * JSON object. A typical result is shown below. We want as much as possible
	 * avoid calling the remote method in order to get the build information so
	 * we try to be clever about when we actually connect to the remote server.
	 * 
	 * <pre>
	 * {
	 *   "actions": [{"causes": [{
	 *     "shortDescription": "Started by user anonymous",
	 *     "userName": "anonymous"
	 *   }]}],
	 *   "artifacts": [],
	 *   "building": false,
	 *   "builtOn": "",
	 *   "changeSet": {
	 *     "items": [],
	 *     "kind": null
	 *   },
	 *   "culprits": [],
	 *   "description": null,
	 *   "duration": 30036,
	 *   "fullDisplayName": "MyTestJob_1 #12",
	 *   "id": "2010-03-29_23-12-59",
	 *   "keepLog": false,
	 *   "number": 12,
	 *   "result": "SUCCESS",
	 *   "timestamp": 1269897179000,
	 *   "url": "http://localhost:8080/job/MyTestJob_1/12/"
	 * }
	 * </pre>
	 */

	@Override
	public synchronized Build getBuild(int number) {
		// Build #0 never exists for any job
		if (number == 0) {
			return null;
		}
		HudsonBuild b = (HudsonBuild) builds.get(number);
		// Connect to the Hudson server and retrieve the build if we don't
		// already have it and update it if it's being built and the data may be
		// old.
		if (b == null || (b.isBuilding() && b.mayBeOld())) {
			try {
				IRemoteCallParameter[] query = RemoteCallParameterFactory
						.createParameters("path", null, "depth", "2");
				IRemoteCallable getBuildQuery = RestCallableFactory
						.createCallable(
								"buildQuery",
								CharEscapers.escapeUriQuery("/job/" + getName()
										+ "/" + number + "/api/json"), query);
				IRemoteServiceClientContainerAdapter adapter = (IRemoteServiceClientContainerAdapter) container
						.getAdapter(IRemoteServiceClientContainerAdapter.class);
				IRemoteServiceRegistration registration = adapter
						.registerCallables(
								new String[] { IHudsonQuery.class.getName() },
								new IRemoteCallable[][] { { getBuildQuery } },
								null);
				IRemoteService restClientService = adapter
						.getRemoteService(registration.getReference());
				Object json = restClientService.callSync(RestCallFactory
						.createRestCall(IHudsonQuery.class.getName()
								+ ".buildQuery"));
				if (json instanceof JSONObject) {
					try {
						if (b == null) {
							builds.put(number, new HudsonBuild(
									(JSONObject) json));
						} else {
							(b).decode((JSONObject) json);
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			} catch (ECFException e) {
				e.printStackTrace();
			}
		}
		return builds.get(number);
	}

	private static class HudsonLogStorage implements IStorage {
		private final String string;
		private final String name;

		HudsonLogStorage(String input, String name) {
			this.string = input;
			this.name = name;
		}

		public InputStream getContents() throws CoreException {
			if (string != null) {
				return new ByteArrayInputStream(string.getBytes());
			} else {
				return new ByteArrayInputStream(new byte[0]);
			}
		}

		public IPath getFullPath() {
			return null;
		}

		public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
			return null;
		}

		public String getName() {
			return name;
		}

		public boolean isReadOnly() {
			return true;
		}
	}

	@Override
	public IStorage getLog() {
		return new HudsonLogStorage((String) execute(
				getLastStartedBuildNumber() + "/consoleText", "getLogAction"),
				getName() + " #" + getLastStartedBuildNumber());
	}

	@Override
	public IStorage getLog(int build) {
		return new HudsonLogStorage((String) execute(build + "/consoleText",
				"getLogAction"), getName() + " #" + build);
	}

	@Override
	public void terminate() {
		execute(getLastStartedBuildNumber() + "/stop", "tempAction");
	}
}
